#!/bin/bash
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source.  A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#

#
# Copyright (c) 2018, Joyent, Inc.
#

#
# Command-line tool for managing nic tags
#

PATH=/usr/bin:/usr/sbin
. /lib/sdc/config.sh
. /lib/sdc/usb-key.sh

CMD=
CONF=
CONF_FILES=
DELIM=":"
EXISTS_NAMES=
HAVE_USB=
LIST_LINE="%-14s %-18s %-14s %-16s\n"
LOCAL=
NO_LIST_STUBS=
PARSABLE=
PROPS=
OVERLAY_RULES="/var/run/smartdc/networking/overlay_rules.json"
TMP_CONF="/tmp/.nic-tags"
VERBOSE=



# --- helpers



function fatal()
{
    echo "Error: $*" >&2
    exit 1
}

function warn()
{
    local msg=$*
    [[ -n $msg ]] && echo "$msg" >&2
}


function verbose()
{
    [[ -n $VERBOSE ]] && echo "+ $*" >&2
}


function usage()
{
    local code msg me
    code=${1:-2}
    shift
    msg=$*
    [[ -n $msg ]] && echo "$msg"
    me=${0##*/}
cat - >&2 <<USAGE
Manage nic tags

Usage:
        $me [OPTIONS] <subcommand> <args...> ...

General Options:
        -v
            Verbose output

        -h
            Print help and exit

        -?
            Print help and exit

Subcommands:
        add             [-l] [-p prop=value,...] <name> [mac]
        delete          [-f] <name>
        exists          [-l] <name>
        list            [-l | -L] [-p] [-d delim]
        update          [-l] [-p prop=value,...] <name> [mac]
        vms             <name>

See nictagadm(1M) for more information
USAGE
    exit "$code"
}

# update sysinfo
function update_sysinfo() {
    verbose 'updating sysinfo'
    sysinfo -u || warn 'failed to update sysinfo'
}

# Clean up any temp files left around
function cleanup()
{
    local conf
    for conf in "${CONF_FILES[@]}"; do
        [[ -f $conf.$$ ]] && rm "$conf.$$"
    done
}


# Returns non-zero if the MAC is invalid
function valid_mac()
{
    local re='^([0-9a-fA-F]{1,2}:){5}[0-9a-fA-F]{1,2}$'
    [[ $1 =~ $re ]]
}


# Returns zero for valid nic tag names, non-zero otherwise. Valid names:
# * Have the same valid characters as vnics and etherstubs
#   (alphanumeric plus _)
# * < 31 characters long, to allow for vnics to be brought up
#   named after those tags (eg: external0, external1, etc)
function valid_name()
{
    local n=$1
    local len=${#n}
    local re='^[a-zA-Z0-9_]+'

    [[ -n $n ]] && (( len < 31 )) && [[ $n =~ $re ]]
}

# Returns zero for a valid overlay rule name, non-zero otherwise. Valid names
# are slightly different. Specifically:
# * Have the same valid characters as vnics and etherstubs
#   (alphanumeric plus _)
# * Must end with a non-number, so they can be created as overlay devices with
#   <tag-name><overlay id>
# * Must be < 20 characters long, to allow for overlays to use a full 32-bit id
#   space
#
function valid_overlay_rule()
{
    local n=$1
    local len=${#n}
    local re='^[a-zA-Z0-9_]*[a-zA-Z_]$'

    [[ -n $n ]] && (( len < 20 )) && [[ $n =~ $re ]]
}


# Returns zero for valid etherstub names, non-zero otherwise. Valid names:
# * Consist only of alphanumeric characters plus _
# * <= 31 characters
# * Have a number at the end (in order to be a valid link name)
function valid_stub_name()
{
    local n=$1
    local len=${#n}
    local re='^[a-zA-Z0-9_]*[0-9]$'

    [[ -n $n ]] && (( len <= 31 )) && [[ $n =~ $re ]]
}

function valid_gen_name()
{
    local n=$1
    local re='^[a-zA-Z0-9_]+'

    [[ -n $n ]] && [[ $n =~ $re ]]
}


# Returns non-zero if the MAC address does not belong to a physical nic on the
# system
function nic_exists()
{
    local out=$(dladm show-phys -pmo address)
    (( $? == 0 )) || fatal 'failed to call dladm'
    local mac
    while read -r mac; do
        [[ $(normalize_mac "$mac") == "$1" ]] && return 0
    done <<< "$out"
    return 1
}


# Returns non-zero if the aggregation does not exist in the config file
function aggr_exists()
{
    local conf_match
    conf_match=$(sdc_config_keys_contain "^$1_aggr$")
    [[ $conf_match == "true" ]] && return 0
    sdc_bootparams_keys | grep -q "^$1_aggr"
}


function get_link_names()
{
    local out=$(dladm show-phys -pmo address,link)
    (( $? == 0 )) || fatal 'failed to call dladm'
    local mac link
    while IFS=: read mac link; do
        mac=$(normalize_mac "$mac")
        LINKS[$mac]=$link
    done <<< "$out"
}


# helper to set global "normalized" to the expanded version of MAC ($1)
function normalize_mac()
{
    local mac=$1 arr new

    [[ -z $mac ]] && fatal "unable to normalize empty mac!"

    # read mac address into array by breaking on ":" characters
    IFS=: read -ra arr <<< "$mac"

    # normalize hex
    printf -v new '%02x:' "${arr[@]/#/0x}"

    # strip off trailing ":"
    new=${new%:}

    # ASSERT what we produced is valid
    valid_mac "$new" || fatal "failed to normalize MAC '$mac'"

    echo "$new"
}

#
# A valid MTU is a four digit number in the range of [1500, 9000]
#
function valid_mtu()
{
    local mtu=$1
    local re='^[0-9]+$'

    [[ $mtu =~ $re ]] || fatal "invalid mtu: $mtu"
    (( mtu >= 1500 && mtu <= 9000 )) || fatal 'mtu must be between 1500-9000'
}


function find_config_paths()
{
    if boot_file_config_enabled; then
        CONF_FILES=("$TMP_CONF")
        CONF=$TMP_CONF
        verbose "Using config file (boot file config enabled): ${CONF_FILES[*]}"
        return
    fi

    USB_CONFIG_COPY="$(svcprop -p 'joyentfs/usb_copy_path' \
        svc:/system/filesystem/smartdc:default)/config"
    USB_MNT="/mnt/$(svcprop -p 'joyentfs/usb_mountpoint' \
        svc:/system/filesystem/smartdc:default)"
    USB_CONFIG="${USB_MNT}/config"

    if [[ -f $USB_CONFIG_COPY ]]; then
        # SmartOS w/o SDC does not have /mnt/usbkey, so when smartos=true, we'll
        # not try to mount / umount a USB Key that won't exist. (OS-4357)
        if bootparams | grep -q '^smartos=true'; then
            HAVE_USB=
            CONF_FILES=("$USB_CONFIG_COPY")
        else
            HAVE_USB=true
            CONF_FILES=("$USB_CONFIG_COPY" "$USB_CONFIG")
        fi
        CONF=$USB_CONFIG_COPY
    elif [[ -f $USB_CONFIG ]]; then
        HAVE_USB=true
        CONF_FILES=("$USB_CONFIG_COPY" "$USB_CONFIG")
        CONF=$USB_CONFIG
    else
        CONF_FILES=("$TMP_CONF")
        CONF=$TMP_CONF
    fi
    verbose "Using config file: ${CONF_FILES[*]}"
}


function mount_usb()
{
    local key typ
    if [[ -z $HAVE_USB ]]; then
        verbose "USB copy not present: not mounting USB key"
        return 0
    fi

    typ=$(awk -v "mnt=$USB_MNT" '$2 == mnt { print $3 }' /etc/mnttab)
    if [[ -n $typ ]]; then
        USB_ALREADY_MOUNTED=true
        verbose "USB key already mounted at: $USB_MNT"
        return 0
    fi

    # prefer sdc-usbkey
    if /opt/smartdc/bin/sdc-usbkey -h &>/dev/null; then
        # calling mount on an already mounted disk is a no-op (or it triggers
        # a remount with the proper options set)
        /opt/smartdc/bin/sdc-usbkey mount || fatal 'failed to mount USB key'
        return 0
    fi

    mount_usb_key || fatal 'failed to mount USB key'

    if [[ ! -f $USB_CONFIG ]]; then
        verbose "$USB_CONFIG does not exist"
        return 0
    fi
}


function umount_usb()
{
    local typ
    if [[ -z $HAVE_USB ]]; then
        verbose "USB copy not present: not unmounting USB key"
        return 0
    fi

    if [[ -n $USB_ALREADY_MOUNTED ]]; then
        verbose "USB key mounted before script was run: not unmounting"
        return 0
    fi

    if /opt/smartdc/bin/sdc-usbkey -h &>/dev/null; then
        /opt/smartdc/bin/sdc-usbkey unmount || fatal 'failed to unmount USB key'
        return 0
    fi

    unmount_usb_key || fatal 'failed to unmount USB key'
}


# For nodes without /usbkey, initialize TMP_CONF with the nic and etherstub
# values from bootparams
function tag_conf_init()
{
    [[ -e $TMP_CONF ]] && return 0

    if boot_file_config_enabled; then
        verbose "Initializing TMP_CONF: ${TMP_CONF} (boot file config enabled)"
        if boot_file_config_valid 2>/dev/null; then
            boot_file_nic_tag_params > "$TMP_CONF"
            chmod 644 "$TMP_CONF"
        fi
    else
        verbose "Initializing TMP_CONF: ${TMP_CONF} (using bootparams)"
        bootparams | egrep '^.*_nic=|^etherstub=' > "$TMP_CONF"
        chmod 644 "$TMP_CONF"
    fi
}


# Check if a tag exists, and return 0 if it does.
#
# This is slightly more complicated for overlay rules. For overlay rules, the
# name of the rule itself is not a tag. Instead, a valid tag is one of which is
# formatted as 'rule/<number>'. As such, we check the tag type and if it matches
# an overlay_rule, we explicitly error. In addition, if it doesn't match and it
# has a '/' in it, then we see if it matches an overlay rule.
function tag_exists()
{
    local typ tag num rule_re
    cmd_tag_list > /dev/null

    #
    # The '/' character is basically toxic to the shell when it comes to
    # variable names. So what we do is if this matches the overlay_rule pattern,
    # then we consider it. Otherwise, we next have to check if it's a valid to
    # make sure that none of the other shell meta-characters get in our way.
    # Isn't this fun? Sigh, shell...
    #
    rule_re='^([a-zA-Z_0-9]+)/([0-9]+)$'
    if [[ $1 =~ $rule_re ]]; then
        tag=${BASH_REMATCH[1]}
        num=${BASH_REMATCH[2]}
        typ=${TAG_TYPES[$tag]}
        [[ $typ == "overlay_rule" ]] || return 1
        (( num < 0 || num > 4294967294 )) && return 1
        return 0
    fi

    valid_gen_name "$1" || return 1

    typ=${TAG_TYPES[$1]}
    [[ $typ == "overlay_rule" ]] && return 1
    [[ -n $typ ]] && return 0

    return 1
}


function tag_in_use()
{
    verbose "checking vms using tag: $1"
    (( $(cmd_tags_used_by "$1" | wc -l) > 0 ))
}


# Moves any temp files over top of the original files they're
# meant to replace
function move_files()
{
    local conf
    for conf in "${CONF_FILES[@]}"; do
        [[ ! -f $conf ]] && continue
        [[ ! -f $conf.$$ ]] && continue

        mv "$conf.$$" "$conf" || fatal "Error moving '$conf.$$' to '$conf"
    done
}


# Adds an etherstub config item to all conf files
function conf_add_etherstub()
{
    local conf stub_line
    verbose "Adding etherstub: $1"

    for conf in "${CONF_FILES[@]}"; do
        if [[ ! -f $conf ]]; then
            verbose "config file '$conf' does not exist: not adding etherstub"
            continue
        fi
        verbose "Adding etherstub '$1' to '$conf'"

        stub_line=$(grep '^etherstub=' "$conf" | tail -n 1)
        grep -v '^etherstub=' "$conf" > "$conf.$$"

        # If the value of the etherstub line is empty, just replace the
        # whole line
        echo "$stub_line" | grep '^etherstub=\s*$' > /dev/null 2>&1
        [[ $? == "0" ]] && stub_line=""

        if [[ -n $stub_line ]]; then
            echo "$stub_line,$1" >> "$conf.$$"
        else
            echo "etherstub=$1" >> "$conf.$$"
        fi
        [[ $? != 0 ]] && fatal "could not write to '$conf.$$'"
    done

    move_files
}

# Adds a nic tag item to all conf files
function conf_add_tag()
{
    local conf
    for conf in "${CONF_FILES[@]}"; do
        if [[ ! -f $conf ]]; then
            verbose "File '$conf' does not exist: not adding tag"
            continue
        fi
        verbose "Adding nic tag: $1=$2 to '$conf'"

        echo "${1}_nic=${2}" >> "$conf"
        [[ $? != 0 ]] && fatal "could not write to '$conf'"
        if [[ -n ${3} ]]; then
            echo "${1}_mtu=${3}" >> "$conf"
            [[ $? != 0 ]] && fatal "could not write to '$conf'"
        fi
    done
}


# Updates a nic tag item in all conf files
function conf_update_field()
{
    local name key value

    name=$1
    key=$2
    value=$3
    for conf in "${CONF_FILES[@]}"; do
        if [[ ! -f $conf ]]; then
            verbose "File '$conf' does not exist: not updating tag"
            continue
        fi
        verbose "Updating nic tag: ${name}_${key}=$value in '$conf'"

        grep -v "^${name}_${key}=" "$conf" > "$conf.$$"
        echo "${name}_${key}=${value}" >> "$conf.$$"
        [[ $? != 0 ]] && fatal "could not write to '$conf'"
    done
}

# Commit a series of updates now that they're all done
function conf_update_commit()
{
    move_files
}


# Deletes a nic tag line from all conf files
function conf_delete_tag()
{
    local conf

    for conf in "${CONF_FILES[@]}"; do
        if [[ ! -f $conf ]]; then
            verbose "File '$conf' does not exist: not deleting tag"
            continue
        fi
        verbose "Deleting nic tag '$1' from '$conf'"

        grep -v "^${1}_nic=" "$conf" | grep -v "^${1}_mtu=" > "$conf.$$"
        [[ $? != 0 ]] && fatal "could not write to '$conf'"
    done

    move_files
}


# Deletes an etherstub from all conf files
function conf_delete_etherstub()
{
    local conf stub_line
    for conf in "${CONF_FILES[@]}"; do
        if [[ ! -f $conf ]]; then
            verbose "File '$conf' does not exist: not deleting etherstub"
            continue
        fi
        verbose "Deleting etherstub '$1' from '$conf'"

        stub_line=$(grep '^etherstub=' "$conf" | tail -n 1)
        grep -v '^etherstub=' "$conf" > "$conf.$$"

        # If there are no more etherstubs left in the line, just omit it
        # from the file
        echo "$stub_line" | grep -q '^etherstub=\s*$'
        [[ $? == "0" ]] && stub_line=""

        if [[ -n $stub_line ]]; then
            echo "$stub_line" | sed -e "s/$1,*//" | sed -e s/,$// >> "$conf.$$"
        fi
    done

    move_files
}

# prints one tag / mac / link line, optionally formatted to be parseable
function print_line()
{
    local tag mac link typ
    tag=$1
    mac=$2
    link=$3
    typ=$4

    # Try to see if we have a link name from get_link_names():
    [[ -z $link ]] && link="${LINKS[$mac]}"
    [[ -z $link ]] && link="-"

    [[ -z $typ ]] && typ=${TAG_TYPES[$tag]}
    ntype=${typ//_/ }

    if [[ -n $PARSABLE ]]; then
        [[ $DELIM == ":" ]] && mac=${mac//:/\\:}
        echo "${tag}${DELIM}${mac}${DELIM}${link}${DELIM}${ntype}"
    else
        printf "$LIST_LINE" "$tag" "$mac" "$link" "${ntype}"
    fi
}

function parse_props()
{
    local props arr prop key value
    props=$1

    # split properties on "," and load into the array "arr"
    IFS=, read -ra arr <<< "$props"

    # loop key=value strings
    for prop in "${arr[@]}"; do
        IFS='=' read -r key value <<< "$prop"
        [[ -n $key && -n $value ]] || fatal "invalid property: $prop"
        PROPS[$key]=$value
    done
}

# --- commands



function cmd_tag_exists()
{
    local arg es
    if (( $# < 1 )); then
        fatal "tag_exists: no tag specified"
    fi

    es=0
    for arg in "$@"; do
        if ! tag_exists "$arg"; then
            if [[ -z $EXISTS_NAMES ]]; then
                 warn "invalid tag: $arg"
            else
                 warn "$arg"
            fi
            es=1
        fi
    done

    return $es
}


function cmd_tag_list()
{
    local tag val key stub_line mac rl conf stub

    [[ -z $PARSABLE ]] && printf "$LIST_LINE" \
        "NAME" "MACADDRESS" "LINK" "TYPE"
    get_link_names

    if [[ -z $LOCAL ]]; then
        # turn conf into associative array
        declare -A conf
        while IFS='=' read -r key value; do
            [[ -n $key && -n $value ]] || continue
            [[ ${key:0:1} == '#' ]] && continue
            conf[$key]=$value
        done < "$CONF"

        # loop config variables related to nic
        nic_re='_nic$'
        for key in "${!conf[@]}"; do
            [[ $key =~ $nic_re ]] || continue
            tag=${key%_nic}
            val=${conf[$key]}

            if valid_stub_name "$val"; then
                TAG_TYPES[$tag]='aggr'
                print_line "$tag" "-" "$val"
            else
                TAG_TYPES[$tag]='normal'
                mac=$(normalize_mac "$val")
                print_line "$tag" "$mac"
            fi
        done
    fi

    #
    # Go through and print overlay device rules
    #
    if [[ -z $LOCAL ]]; then
        while read -r rl; do
            ! valid_overlay_rule "$rl" && continue
            TAG_TYPES[$rl]='overlay_rule'
            print_line "$rl" "-" "-"
        done < <(json -kaf "$OVERLAY_RULES" 2>/dev/null)
    fi

    [[ -n $NO_LIST_STUBS ]] && return 0

    # create a union of the stubs found in dladm show-etherstub
    # and the config
    declare -A stubs_union

    # read dladm
    while read -r stub; do
        STUBS_UP[$stub]=true
        stubs_union[$stub]=true
        TAG_TYPES[$stub]='etherstub'
    done < <(dladm show-etherstub -p)

    # read the config
    stub_line=$(grep '^etherstub=' "$CONF" | tail -n 1)
    if [[ -n $stub_line ]]; then
        IFS='=' read -r _ value <<< "$stub_line"
        IFS=, read -ra values <<< "$value"

        for stub in "${values[@]}"; do
            stubs_union[$stub]=true
            TAG_TYPES[$stub]='etherstub'
        done
    fi

    # loop everything found after sorting
    while read -r stub; do
        [[ -n $stub ]] || continue
        print_line "$stub" '-'
    done < <(printf '%s\n' "${!stubs_union[@]}" | sort)
}


function cmd_tag_add()
{
    local name mac mtu

    name=$1
    [[ -z $name ]] && usage

    [[ -n $2 && -n ${PROPS['mac']} ]] && fatal "mac address specified twice"
    [[ -n $2 ]] && mac=$2
    [[ -n ${PROPS['mac']} ]] && mac=${PROPS['mac']}

    [[ -z $mac && -z $LOCAL ]] && fatal \
        "create non-local tag, but no mac address specified"

    [[ -n $mac && -n $LOCAL ]] && fatal \
        "create local tag, but mac address specified"

    mtu=${PROPS['mtu']}
    [[ -n $mtu && -n $LOCAL ]] && fatal "Cannot specify MTU for local nic tag"
    [[ -n $mtu ]] && valid_mtu "$mtu"

    tag_exists "$name" && fatal "nic tag '$name' already exists"

    #
    # When creating a network tag over an aggregation, we use the 'MAC'
    # field to optionally be the name of an aggregation.
    #
    if [[ -n $mac ]]; then
        if valid_stub_name "$mac"; then
            aggr_exists "$mac" || fatal "aggregation '$mac' does not exist"
        else
            valid_mac "$mac" || fatal "MAC address '$mac' is invalid"
            mac=$(normalize_mac "$mac")
            nic_exists "$mac" || \
                fatal "No physical nic found with MAC address '$mac'"
        fi

        valid_name "$name" || "nic tag name is invalid"
    else
        valid_stub_name "$name" || fatal "nic tag name is invalid"
    fi


    verbose "adding nic tag: name='$name', mac='$mac', local='$LOCAL'"
    [[ -n $mtu ]] && verbose "adding mtu for tag: name='$name', mtu='$mtu'"

    mount_usb
    if [[ -n $LOCAL ]]; then
        conf_add_etherstub "$name"
        dladm create-etherstub -t "$name" || \
            fatal "Could not create etherstub '$name'"
    else
        conf_add_tag "$name" "$mac" "$mtu"
    fi

    update_sysinfo
    umount_usb

    [[ -n $mtu ]] && warn "MTU changes will not take effect until next reboot"
    return 0
}


function cmd_tag_update()
{
    local name mac mtu

    name=$1
    [[ -z $name ]] && usage
    [[ -n $LOCAL ]] && fatal "cannot update a local tag"

    #
    # We require either the presence of the MTU or mac address, though
    # both are also fine.
    #
    [[ -n $2 && -n ${PROPS['mac']} ]] && fatal "mac address specified twice"
    [[ -n $2 ]] && mac=$2
    [[ -n ${PROPS['mac']} ]] && mac=${PROPS['mac']}

    mtu=${PROPS['mtu']}
    [[ -n $mtu ]] && valid_mtu "$mtu"

    [[ -z $mac && -z $mtu ]] && fatal "nothing to update"

    tag_exists "$name" || fatal "nic tag '$name' does not exist"

    #
    # Recall that the 'mac' of an aggregation is its name.
    #
    if [[ -n $mac ]]; then
        if valid_stub_name "$mac"; then
            aggr_exists "$mac" || fatal "aggregation '$mac' does not exist"
        else
            valid_mac "$mac" || fatal "MAC address '$mac' is invalid"
            mac=$(normalize_mac "$mac")
            nic_exists "$mac" || fatal \
                "No physical nic found with MAC address '$mac'"
        fi
    fi

    [[ -n $mac ]] && verbose \
        "updating nic tag MAC: name='$name', mac='$mac'"
    [[ -n $mtu ]] && verbose \
        "updating nic tag MTU: name='$name', mtu='$mtu'"

    mount_usb
    [[ -n $mac ]] && conf_update_field "$1" "nic" "$mac"
    [[ -n $mtu ]] && conf_update_field "$1" "mtu" "$mtu"
    conf_update_commit

    update_sysinfo
    umount_usb

    [[ -n $mtu ]] && warn "MTU changes will not take effect until next reboot"
    return 0
}


function cmd_tag_delete()
{
    local tag_type
    [[ -z $1 ]] && usage
    [[ -n $LOCAL ]] && usage

    tag_exists "$1" || fatal "nic tag '$1' does not exist"
    [[ -z $FORCE ]] && tag_in_use "$1" && fatal "nic tag '$1' is in use"
    verbose "deleting nic tag: $1"

    mount_usb

    tag_type=${TAG_TYPES[$1]}
    if [[ $tag_type == "etherstub" ]]; then
        conf_delete_etherstub "$1"
    else
        conf_delete_tag "$1"
    fi

    if [[ -n ${STUBS_UP[$1]} ]]; then
        verbose "bringing down etherstub: $1"
        dladm delete-etherstub "$1" || fatal "could not remove etherstub '$1'"
    fi

    update_sysinfo
    umount_usb
}


function cmd_tags_used_by()
{
    [[ -z $1 ]] && usage
    vmadm list -p -o uuid "nics.*.nic_tag=$1"
}



# --- main



[[ $(zonename) != 'global' ]] && \
    fatal 'This program can only be run in the global zone.'
[[ $EUID != 0 ]] && \
    fatal 'This program can only be run as root.'

# -v goes before the command
while getopts vh? opt; do
    case $opt in
    v)
        VERBOSE="true"
        ;;
    h)
        usage 0
        ;;
    *)
        usage 2
        ;;
    esac
done
shift "$(( OPTIND - 1 ))"
OPTIND=1

# Wait until after the VERBOSE has been set:
load_sdc_config
find_config_paths
trap cleanup EXIT

# If we have no /usbkey, ensure that the tags conf file is initialized
if [[ -z $HAVE_USB && ! -f $USB_CONFIG_COPY ]]; then
    tag_conf_init
fi

# parsed output of '-p key=value,key2=value2,...'
# e.g. PROPS['key'] == 'value', PROPS['key2'] == 'value2', ...
declare -A PROPS

# mapping of normalized mac addresses => link name, e.g.:
# LINKS['f4:6d:04:04:51:d4'] == 'rge0'
# LINKS['00:04:23:bd:18:29'] == 'e1000g0'
declare -A LINKS

# etherstubs that are up (in dladm)
# e.g. if [[ -n ${STUBS_UP['foobar0']} ]]; then ...; fi
declare -A STUBS_UP

# mapping of tag names to their type, e.g.:
# TAG_TYPES['admin'] == 'normal'
# TAG_TYPES['foobar0'] == 'etherstub'
# possible types: normal, etherstub, aggr, overlay_rule.
declare -A TAG_TYPES

CMD=$1
shift
case "$CMD" in
add|update)
    while getopts "lp:" c; do
        case "$c" in
        l)
            LOCAL="true"
            ;;
        p)
            parse_props "$OPTARG"
            ;;
        :)
            usage 2 "missing required argument -- $OPTARG"
            ;;
        *)
            usage 2 "invalid option: $OPTARG"
            ;;
        esac
    done
    ;;
delete)
    while getopts "f" c; do
        case "$c" in
        f)
            FORCE="true"
            ;;
        :)
            usage 2 "missing required argument -- $OPTARG"
            ;;
        *)
            usage 2 "invalid option: $OPTARG"
            ;;
        esac
    done
    ;;
list)
    while getopts "lLpd:" c; do
        case "$c" in
        d)
            DELIM=$OPTARG
            delim_flag=true
            ;;
        l)
            LOCAL="true"
            ;;
        L)
            NO_LIST_STUBS="true"
            ;;
        p)
            PARSABLE="true"
            ;;
        :)
            usage 2 "missing required argument -- $OPTARG"
            ;;
        *)
            usage 2 "invalid option: $OPTARG"
            ;;
        esac
    done
    ;;
exists)
    while getopts "l" c; do
        case "$c" in
        l)
            EXISTS_NAMES="true"
            ;;
        :)
            usage 2 "missing required argument -- $OPTARG"
            ;;
        *)
            usage 2 "invalid option: $OPTARG"
            ;;
        esac
    done
    ;;
esac

shift "$(( OPTIND - 1 ))"

[[ -n $LOCAL ]] && [[ -n $NO_LIST_STUBS ]] && \
    fatal "Cannot specify both local and non-local options"

[[ -n $delim_flag ]] && [[ -z $PARSABLE ]] && \
    fatal "Delimiter option requires parsable option to be set"

if [[ -z "$CMD" ]]; then
    usage 2 "no subcommand given"
fi

case "$CMD" in
add)
    cmd_tag_add "$1" "$2"
    ;;
delete)
    cmd_tag_delete "$1"
    ;;
exists)
    cmd_tag_exists "$@"
    ;;
help)
    usage 0
    ;;
list)
    cmd_tag_list
    ;;
update)
    cmd_tag_update "$1" "$2"
    ;;
vms)
    cmd_tags_used_by "$1"
    ;;
*)
    fatal "unknown command ${CMD}"
    ;;
esac
